home *** CD-ROM | disk | FTP | other *** search
/ Workbench Add-On / Workbench Add-On - Volume 1.iso / Dev / Oberon / texts / OP2.Paper.Text < prev    next >
Text File  |  1994-03-10  |  26KB  |  569 lines

  1. OP2: A PORTABLE OBERON-2 COMPILER
  2.  
  3. Presented at the 2nd International Modula-2 Conference, Loughborough,
  4. Sept 91
  5.  
  6. Regis CRELIER
  7. Institut für Computersysteme, ETH Zürich
  8.  
  9. ABSTRACT
  10.  
  11. A portable compiler for the language Oberon-2 is presented. Most related
  12. works pay for portability with low compilation speed or poor code
  13. quality. Portability and efficiency have been given the same importance
  14. in our approach. Hence, an automated retargetable code generation has
  15. not been considered. The compiler consists of a front-end and a
  16. back-end. The front-end does the lexical and syntactic analysis,
  17. including type checking. It builds a machine-independent structure
  18. representing the program. This structure is made up of a symbol table
  19. and an abstract syntax tree, rather than a stream of pseudo-instructions
  20. coded in an "intermediate language". If no errors are found, control is
  21. passed to the back-end which generates code from this intermediate
  22. structure. This structure clearly separates the front-end which is
  23. machine-independent from the back-end which is machine-dependent. While
  24. the front-end can remain unchanged, the back-end has to be reprogrammed,
  25. when the compiler is retargeted to a new machine.
  26.  
  27. This compiler has been successfully used to port the Oberon System onto
  28. different computers. Code generators have been implemented both for CISC
  29. and RISC processors. Differences in processor architectures are
  30. reflected in the complexity of the back-end, the generated code density
  31. and performance. The compiler is written in Oberon. New compilers have
  32. therefore to be first compiled on an already working Oberon System. If
  33. such a system is not available, a version of the compiler whose back-end
  34. produces C code may be used for the bootstrap.
  35.  
  36. The compilation techniques presented here are not restricted to Oberon
  37. compilers, but could be used for other programming languages too.
  38. Nevertheless, Oberon and OP2 tend to the same ideal: simplicity,
  39. flexibility, efficiency and elegance.
  40.  
  41. INTRODUCTION
  42.  
  43. Portability is an important criterion for program quality. A compiler is
  44. a program as well, and it may be ported. If it should produce the same
  45. code as before on the new machine (cross compiler), then it is not more
  46. difficult to port it than any other program also written in a higher
  47. programming language. But if the produced code must run on the new
  48. machine, the compiler has to be rewritten and it is not the same program
  49. any more. In that sense, the compiler is not, and cannot be, portable.
  50. By the term portable compiler , we refer here to a compiler that needs
  51. reasonably small effort to be adapted to a new machine and/or to be
  52. modified to produce new code. Most related work attempt to reduce this
  53. adaptation cost to a minimum, compromising the compilation speed and the
  54. code quality. A classification of such automated retargetable code
  55. generation techniques and a survey of the works on those techniques are
  56. presented in [1]. The basic idea is to produce code for a virtual
  57. machine. This code is then expanded into real machine instructions. The
  58. expansion can be done by hand-written translators [2] or by a
  59. machine-independent code generation algorithm, in which case each
  60. intermediate language instruction [3] or each recognized pattern of
  61. these [4] is expanded into a fixed pattern of target machine
  62. instructions recorded in tables. Trees may replace linear code to feed
  63. the pattern matching algorithm [5, 6, 7]. These techniques usually yield
  64. poor code quality, making a peephole optimization phase necessary, which
  65. further increases the compilation time.
  66.  
  67. In our approach, we tried to find the right balance between code
  68. quality, compilation speed and portability. We think it is worth-while
  69. investing, say three man-months, for a port, if the resulting compiler
  70. is very fast and produces efficient code. Thus, a pattern matching or
  71. table-driven code generation has not been considered. Instead, we looked
  72. at more conventional and faster techniques, such as single-pass
  73. compilation [8, 9]. In a single-pass compiler, the compilation phases
  74. are executed simultaneously. No intermediate representation of the
  75. source text is needed between the different phases, making the compiler
  76. compact and efficient, but not very portable. Indeed, since
  77. machine-dependent and machine-independent phases are mixed up, it is
  78. very difficult to modify the compiler for a new machine.
  79.  
  80. One solution to the problem is to clearly separate the compilation
  81. phases into two groups: the front-end consisting of the
  82. machine-independent phases (lexical and syntactic analysis, type
  83. checking) and the back-end consisting of the machine-dependent phases
  84. (storage allocation, code generation). Only the back-end must be
  85. modified when the compiler is ported. The front-end enters declarations
  86. in a symbol table and builds an intermediate representation of the
  87. program statements, an abstract syntax tree. If no errors were found,
  88. control is passed to the back-end, which generates code from the syntax
  89. tree. Since this structure is guaranteed to be free of errors, type
  90. checking or error recovery are not part of the back-end, which is a
  91. noteworthy advantage. Only implementation restrictions must be checked.
  92. Another advantage of the intermediate structure is that optional passes
  93. may be inserted to optimize the code. Such an optimization phase cannot
  94. be easily embedded in a conventional single-pass compiler. The front-end
  95. and the back-end are implemented separately as a set of modules.
  96.  
  97. MODULE STRUCTURE
  98.  
  99. Originally, OP2 has been designed to compile Oberon programs [10] and
  100. has been slightly modified later to compile Oberon-2 programs [11, 12].
  101. It consists of nine modules (see figure 1) all written in Oberon.
  102.  
  103. The lowest module of the hierarchy is OPM , where M stands for machine.
  104. We must distinguish between the host machine on which the compiler is
  105. running, and the target machine for which the compiler is generating
  106. code. Most of the time, the two machines are the same, except during a
  107. bootstrap or in case of a cross-compiler. The module OPM defines and
  108. exports several constants used to parametrize the front-end. Some of
  109. these constants reflect target machine characteristics or implementation
  110. restrictions. For example, these values are used in the front-end to
  111. check the evaluation of constant expressions on overflow. But OPM has a
  112. second function. It works as interface between the compiler and the host
  113. machine. This interface includes procedures to read the text to be
  114. compiled, to read and write data in symbol files [13], and to display
  115. text (error messages e.g.) onto the screen. All these input and output
  116. operations are strongly dependent on the operating system. If the
  117. compiler resides in the Oberon System environment [14, 15], the
  118. host-dependent part of OPM remains unchanged.
  119.  
  120. The topmost module (OP2) is very short. It is the interface to the user,
  121. and therefore host machine-dependent. It first calls the front-end with
  122. the source text to be compiled as parameter. If no error is detected, it
  123. then calls the back-end with the root of the tree that was returned by
  124. the front-end as parameter.
  125.  
  126. Figure 1.    Module import graph (an arrow from A to B means B imports A)
  127.  
  128. Between the highest and the lowest module, one finds the front-end and
  129. the back-end, which consist of four, respectively three modules. There
  130. is no interaction during compilation between these two sets of modules.
  131. The symbol table and the syntax tree are defined in module OPT and are
  132. used by both the front-end and the back-end. This explains the presence
  133. of import arrows from OPT to back-end modules visible in the import
  134. graph (figure 1). But there is no transfer of control, such as procedure
  135. calls.
  136.  
  137. The front-end is controlled by the module OPP, a recursive-descent
  138. parser. Its main task is to check syntax and to call procedures to
  139. construct the symbol table and the syntax tree. The parser requests
  140. lexical symbols from the scanner (OPS) and calls procedures of OPT,
  141. the symbol table handler, and of OPB, the syntax tree builder. OPB
  142. also checks type compatibility.
  143.  
  144. The back-end is controlled by OPV, the tree traverser. It first
  145. traverses the symbol table to enter machine-dependent data (using OPM
  146. constants), such as the size of types, the address of variables or the
  147. offset of record fields. It then traverses the syntax tree and calls
  148. procedures of OPC (code emitter), which in turn synthesizes machine
  149. instructions using procedures of OPL (low-level code generator).
  150.  
  151. This module structure achieves to make the front-end target-independent
  152. as well as host-independent, and to make the back-end host-independent.
  153.  
  154. SYMBOL TABLE
  155.  
  156. The symbol table contains information about declared constants,
  157. variables, types and procedures. It is built by the front-end. The
  158. front-end uses it to check the context conditions of the language and
  159. the back-end retrieves type information from it. The symbol table is a
  160. dynamically allocated data structure with three different element types:
  161.  
  162. TYPE
  163.   Const = POINTER TO ConstDesc;
  164.   Object = POINTER TO ObjDesc;
  165.   Struct = POINTER TO StrDesc;
  166.  
  167. An Object is a record (more exactly a pointer to a record), which
  168. represents a declared, named object. The object declaration in the
  169. compiler is the following:
  170.  
  171. ObjDesc = RECORD
  172.   left, right, link, scope: Object;
  173.   name: OPS.Name;
  174.   (* key *)
  175.   leaf: BOOLEAN;
  176.   (* procedure: leaf; variable: candidate to be allocated in register *)
  177.   mode: SHORTINT;
  178.   (* constant, type, variable, procedure or module *)
  179.   mnolev: SHORTINT;
  180.   (* imported from module -mnolev, or local at procedure nesting level mnolev *)
  181.   vis: SHORTINT;
  182.   (* not exported, exported, read-only exported *)
  183.   typ: Struct;
  184.   (* object type *)
  185.   conval: Const;
  186.   (* numeric attributes *)
  187.   adr, linkadr: LONGINT
  188.   (* storage allocation *)
  189. END ;
  190.  
  191. The name of the object stored in the object itself (field name) is used
  192. as key to retrieve the object in its scope. Each scope is organized as a
  193. sorted binary tree of objects (fields left and right) and is anchored
  194. (field scope) to the owner procedure, which in turn belongs as object
  195. to the enclosing scope. Parameters of the same procedure, fields of the
  196. same record and variables of the same scope are additionally linked
  197. together (field link) to maintain the declaration order. The flag leaf
  198. indicates whether a procedure is a leaf procedure or whether a variable
  199. is a candidate to be allocated permanently in a register. The back-end
  200. may or may not use this information - Note that this information could
  201. not be available in a single-pass compiler without intermediate
  202. representation of the program. An object has always a type (field typ),
  203. which is described by a record named StrDesc :
  204.  
  205. StrDesc = RECORD
  206.   form, comp: SHORTINT;
  207.   (* basic or composite type, type class *)
  208.   mno: SHORTINT;
  209.   (* imported from module mno *)
  210.   extlev: SHORTINT;
  211.   (* record extension level *)
  212.   ref, sysflag: INTEGER;
  213.   (* export reference *)
  214.   n, size: LONGINT;
  215.   (* number of elements and allocation size *)
  216.   tdadr, offset: LONGINT;
  217.   (* address of type descriptor *)
  218.   txtpos: LONGINT;
  219.   (* text position *)
  220.   BaseTyp: Struct;
  221.   (* base record type or array element type *)
  222.   link: Object;
  223.   (* record fields or formal parameters of procedure type *)
  224.   strobj: Object
  225.   (* named declaration of this type *)
  226. END ;
  227.  
  228. There are several classes of types: basic types like character, integer
  229. or set, and composite types like array, open array or record (fields
  230. form and comp). The third element type of the symbol table is ConstDesc.
  231. This record contains numeric attributes of objects, like values of
  232. declared or anonymous constants:
  233.  
  234. ConstDesc = RECORD
  235.   ext: ConstExt;
  236.   (* extension for string constant *)
  237.   intval: LONGINT;
  238.   intval2: LONGINT;
  239.   setval: SET;
  240.   realval: LONGREAL
  241. END ;
  242.  
  243. An example is shown in figure 2 below:
  244.  
  245. CONST
  246.   Pi = 3.14;
  247. TYPE
  248.   A = ARRAY 4 OF LONGINT;
  249. VAR
  250.   i: INTEGER;
  251.   a: A;
  252.   x, y: LONGINT;
  253.  
  254. Figure 2.    Declarations and corresponding symbol table
  255.  
  256. SYNTAX TREE
  257.  
  258. The front-end builds an abstract syntax tree representing all statements
  259. of the program. The Oberon syntax is mapped into a binary tree of
  260. elements called NodeDesc :
  261.  
  262. Node = POINTER TO NodeDesc;
  263. NodeDesc = RECORD
  264.   left, right, link: Node;
  265.   class, subcl: SHORTINT;
  266.   readonly: BOOLEAN;
  267.   typ: Struct;
  268.   obj: Object;
  269.   conval: Const
  270. END ;
  271.  
  272. A binary tree has been chosen because each Oberon construct can be
  273. decomposed into a root element identifying the construct and two
  274. subtrees representing its components: an operator has a left and a right
  275. operand, an assignment has a left and a right side, a While statement
  276. has a condition and a sequence of statements, and so on. Some Oberon
  277. constructs are organized sequentially: there are lists of actual
  278. parameters in procedure calls and sequences of statements in structured
  279. statements. It would be expensive to insert dummy nodes to link these
  280. subtrees; an additional link field in the node is much cheaper.
  281.  
  282. Each node has a class (field class) and possibly a subclass (field
  283. subcl) identifying the represented Oberon construct. Each node has a
  284. type, which is a pointer (field typ) to a StrDesc of the symbol table.
  285. Similarly, a leaf node representing a declared object contains a pointer
  286. (field obj) to the corresponding ObjDesc of the symbol table. A
  287. ConstDesc may be attached (field conval) to a node to describe a
  288. numeric attribute, such as the value of an anonymous constant. A
  289. ConstDesc denoting the position in the source text is anchored to the
  290. root node of each statement. This allows locating compilation errors
  291. reported by the back-end. Figure 3 shows the representation of two
  292. statements manipulating variables declared in Figure 2.
  293.  
  294. Figure 3.    Statements and corresponding syntax tree
  295.  
  296. The rules of numeric type compatibility are flexible in Oberon; for
  297. example, it is possible to multiply integers with long integers, as
  298. shown in Figure 3. Note the conversion operator inserted in the tree,
  299. freeing the back-end from type checking.
  300.  
  301. While generating code for a node, one typically has to recursively
  302. evaluate left and right subtrees, then the node itself, and finally the
  303. linked successors if any. A traversal of the tree looks like this:
  304.  
  305. Traverse(node: Node):
  306.  
  307. WHILE node # NIL DO
  308.   Traverse(node^.left);
  309.   Traverse(node^.right);
  310.   Do something with node;
  311.   node := node^.link
  312. END
  313.  
  314. The intermediate representation could be a stream of instructions for a
  315. virtual machine; we have preferred an internal abstract syntax tree for
  316. different reasons. A virtual machine instruction set should be defined
  317. without knowing anything about the future target machines. Perhaps the
  318. mapping of this instruction set to a real instruction set would not be
  319. easy, the virtual and real machines being very different (RISC vs. CISC
  320. e.g.). Generating these pseudo-instructions already needs a code
  321. generator, whereas building the syntax tree is a trivial recursive task
  322. easily embedded in the recursive-descent parser. Since the tree is a
  323. natural mapping of the Oberon syntax, each procedure of the parser
  324. returns as parameter the root of the subtree corresponding to the
  325. construct just parsed by this procedure. Furthermore, the tree keeps the
  326. program structure intact, so that a control-flow analysis necessary for
  327. an optimization phase can be done easily. Without a tree, this analysis
  328. would be expensive, since basic blocks would have been dissolved in the
  329. linear code. The reordering of program pieces is easier to perform in a
  330. tree than in an instruction stream; for example, the conditional
  331. expression of a While statement may be evaluated at the end of the loop,
  332. while first generating code for the right subtree of the While node, and
  333. then for its left subtree. The syntax tree has nevertheless one drawback
  334. over an intermediate linear code: it needs quite a large heap space,
  335. but, nowadays, this is not a real problem any more.
  336.  
  337. CODE GENERATION
  338.  
  339. Although an extensive experience in compiler development is not
  340. necessary to write a new back-end, knowledge of the subject is
  341. nevertheless an advantage. The front-end, which doesn't need any
  342. modifications, has only to be adapted to the new target machine by
  343. editing constants exported from module OPM. Then, the storage allocation
  344. strategy must be adapted to the data alignment requirements of the new
  345. processor. This allocation is done in module OPV, where a procedure
  346. traverses the symbol table and distributes addresses to variables,
  347. offsets to record fields, sizes to structured types, and so on. The last
  348. thing to do, but not least, is to rewrite the code generator. After
  349. storage allocation, code generation takes place. Between the two phases,
  350. OPT shortly gets control in order to produce a symbol file. The whole
  351. process of code generation can be viewed as the process of computing
  352. attribute values for each node of the tree. Attribute values of a node
  353. depend on the attribute values of the children nodes and on the node's
  354. class and type. Hence, module OPV recursively traverses the syntax tree
  355. and calls, in a post-order fashion, procedures of underlying modules
  356. computing attribute values for each traversed node, or subtree of nodes.
  357. These procedures act similarly to an attribute evaluator of an attribute
  358. grammar. The emission of code is actually a side effect of computing
  359. these attributes. Since the side effect (code) is more important than
  360. the computed attribute values, and since these values will only be
  361. reused later to compute the attribute values of parent nodes, they don't
  362. need to be stored in the tree. Instead, they are passed as procedure
  363. parameters named Item during the recursive traversal of the tree.
  364.  
  365. An Item is a record of attributes representing the operand or the result
  366. of an operation. It indicates where the operand is located (memory,
  367. register or immediate value e.g.). Items make it possible to delay
  368. emission of code, so that processor addressing modes can be optimally
  369. used. There are as many Item modes as processor addressing modes.
  370. Depending on the mode specified by a field of the Item, other
  371. attributes like type, address, offset, register number, value of
  372. constant are stored in Item fields as well. The complexity of a
  373. processor architecture is reflected in the declaration of the Item.
  374. Typically, back-ends for CISC processors have Items with many fields and
  375. many possible modes, whereas the ones for RISC are very simple.
  376.  
  377. Since almost each procedure of the code generator has to distinguish
  378. between the different Item modes, the complexity of the back-end depends
  379. on the number of modes. Expression evaluation for RISC processors is
  380. therefore very easy to code. A more difficult part of RISC back-ends is
  381. the register allocation. The advantage in performance of RISC over CISC
  382. may be lost if a too simple allocation strategy is used.
  383.  
  384. An excerpt of the module OPV is listed below, giving an idea how the
  385. back-end works:
  386.  
  387. PROCEDURE^ expr(n: OPT.Node; VAR x: OPL.Item);
  388. (* forward declaration *)
  389.  
  390. PROCEDURE design(n: OPT.Node; VAR x: OPL.Item);
  391.   VAR y: OPL.Item;
  392. BEGIN
  393.   CASE n^.class OF
  394.     ...
  395.     | index: design(n^.left, x); expr(n^.right, y); OPC.Index(x, y)
  396.     (* x := x[y] *)
  397.     ...
  398.   END ;
  399.   x.typ := n^.typ
  400. END design;
  401.  
  402. PROCEDURE expr(n: OPT.Node; VAR x: OPL.Item);
  403.   VAR y: OPL.Item;
  404. BEGIN
  405.   CASE n^.class OF
  406.     ...
  407.     | dyadic:
  408.       expr(n^.left, x); ... expr(n^.right, y);
  409.       CASE n^.subcl OF
  410.       ...
  411.       | plus: OPC.Add(x, y)
  412.         (* x := x + y *)
  413.       ...
  414.       END ;
  415.     ...
  416.   END
  417.   x.typ := n^.typ
  418. END expr;
  419.  
  420. PROCEDURE stat(n: OPT.Node);
  421.   VAR x: OPL.Item; L0, L1: OPL.Label;
  422. BEGIN
  423.   WHILE n # NIL DO
  424.     CASE n^.class OF
  425.       ...
  426.       | while:
  427.         L0 := OPL.pc;
  428.         (* remind loop beginning *)
  429.         expr(n^.left, x);
  430.         (* evaluate conditional expression into x *)
  431.         OPC.CFJ(x, L1);
  432.         (* if not x then jump to L1 *)
  433.         stat(n^.right);
  434.         (* do statement sequence *)
  435.         OPC.BJ(L0);
  436.         (* backwards jump to L0 *)
  437.         OPL.FixLink(L1)
  438.         (* fix-up L1 with current pc *)
  439.       ...
  440.     END ;
  441.     n := n^.link
  442.   END
  443. END stat;
  444.  
  445. MEASUREMENTS AND BENCHMARKS
  446.  
  447. The front-end of OP2 (modules OPS, OPT, OPB and OPP), which remains the
  448. same for all versions of OP2, consists of less than 3500 lines of Oberon
  449. source code. The length of the back-end and the size of the machine code
  450. depend on the target processor architecture.
  451.  
  452. The very first back-end for OP2 was written by the author in less than
  453. three months for the National Semiconductor NS32532 processor used in
  454. the Ceres workstation developed at ETH [16]. The newest version
  455. (Oberon-2) of this back-end (OPL, OPC and OPV) for that machine consists
  456. of less than 2500 lines of Oberon source code, of which about 500 are
  457. portable (module OPV). The total size of the compiler (including modules
  458. OPM and OP2) is less than 6500 lines.
  459.  
  460. About 2000 lines (30% of the compiler) have to be rewritten when the
  461. compiler is ported. This number may vary slightly depending on the
  462. target architecture. For the MIPS R2000, for example, the back-end is
  463. 500 lines longer. The heap space required to store the syntax tree of a
  464. module depends only on the size of the module, but not on the target
  465. processor architecture; it is about 8 times larger than the source text
  466. of the module. Larger variations are noticed in the size of the machine
  467. code: the whole compiler for NS32532 is 62KB, whereas the one for the
  468. MIPS R2000 is 152KB. The different architecture is not only reflected in
  469. code density, but in performance too: on a Ceres (NS32532, 25 MHz), it
  470. takes 32 seconds to recompile OP2; on a DECstation 5000 (MIPS R3000, 25
  471. MHz), only 6.3 seconds. Note that self-compilation time is a good
  472. indicator for compiler quality, since speed, compactness and code
  473. quality are multiplicative factors contributing to the overall result.
  474.  
  475. Several other code generators have been implemented at the Institut für
  476. Computersysteme. They have been used to port the Oberon System to
  477. different computers (Sun SPARCstation, Macintosh, DECstation, IBM PS/2
  478. and S/6000) [17, 18, 19]. In most cases, the new back-ends have been
  479. developed on the Ceres workstation in about three or four months by a
  480. single person. The object files, cross-compiled on Ceres, have been then
  481. transferred to the target machine using a diskette or an RS232 line. We
  482. also have a special back-end producing C code. The idea here is not to
  483. shirk the task of writing a code generator, but to use this Oberon-to-C
  484. translator as a preprocessor for a C compiler during the bootstrap of
  485. the compiler only. Remember that OP2 is written in Oberon; so, if there
  486. is no machine with a running Oberon compiler at immediate disposal, the
  487. new OP2 cannot be cross-compiled. So we execute the bootstrap on the
  488. target machine directly. We first translate the new OP2 to C and then
  489. compile it using an existing C compiler. After a self-compilation step
  490. of OP2, we can and should forget the C compiler.
  491.  
  492. ACKNOWLEDGEMENTS
  493.  
  494. The single-pass Oberon compiler of N. Wirth for Ceres has been a model
  495. of efficiency and compactness for this project. The advice of M. Odersky
  496. helped a lot during the design phase. J. Templ, M. Franz and H.
  497. Mössenböck made valuable comments and improvement suggestions on OP2 and
  498. on this paper. Many thanks to all of them and to the very first users
  499. who searched desperately in their programs for bugs hidden in OP2.
  500.  
  501. REFERENCES AND FURTHER READING
  502.  
  503. 1. Ganapathi M., Fischer C. N., Hennessy J. L., Retargetable Compiler
  504. Code Generation , Computing Surveys 14:4, 573-592, 1982.
  505.  
  506. 2. Amman U., Jensen K., Nïgeli H., Nori K., The Pascal 'P' Compiler:
  507. Implementation Notes , Departement Informatik, ETH Zürich, 1974.
  508.  
  509. 3. Tanenbaum A. S., Kaashoek M. F., Langendoen K. G., Jacobs C. J. H.,
  510. The Design of Very Fast Portable Compilers , ACM SIGPLAN Notices 24:11,
  511. 125-131, 1989.
  512.  
  513. 4. Glanville R. S., Graham S. L., A New Method for Compiler Code
  514. Generation , Fifth ACM Symposium on Principles of Programming Languages,
  515. 231-240, 1978.
  516.  
  517. 5. Aho A. V., Ganapathi M., Tjiang S. W. K., Code Generation Using Tree
  518. Matching and Dynamic Programming , ACM Transactions on Programming
  519. Languages and Systems 11:4, 491-516, 1989.
  520.  
  521. 6. Cattell R. G. G., Newcomer J. M., Leverett B. W., Code Generation in
  522. a Machine-Independent Compiler , ACM SIGPLAN Notices 14:8, 65-75, 1979.
  523.  
  524. 7. Fraser C. W., Wendt A., Integrating Code Generation and Optimization
  525. , ACM SIGPLAN Notices 21:6, 242-248, 1986.
  526.  
  527. 8. Wirth N., A Fast and Compact Compiler for Modula-2 , Report 64,
  528. Departement Informatik, ETH Zürich, 1985.
  529.  
  530. 9. Wirth N., Compilerbau, Eine Einführung , B. G. Teubner Stuttgart,
  531. 1986.
  532.  
  533. 10. Wirth N., From Modula to Oberon, The Programming Language Oberon
  534. (revised edition) , Report 143, Departement Informatik, ETH Zürich,
  535. 1990.
  536.  
  537. 11. Mössenböck H., Differences between Oberon and Oberon-2, The
  538. Programming Language Oberon-2 , Report 160, Departement Informatik, ETH
  539. Zürich, 1991.
  540.  
  541. 12. Mössenböck H., Object-Oriented Programming in Oberon-2 , Second
  542. International Modula-2 Conference, Loughborough, 1991.
  543.  
  544. 13. Gutknecht J., Compilation of Data Structures: A New Approach to
  545. Efficient Modula-2 Symbol Files , Report 64, Departement Informatik, ETH
  546. Zürich, 1985.
  547.  
  548. 14. Wirth N., Gutknecht J., The Oberon System , Report 88, Departement
  549. Informatik, ETH Zürich, 1988.
  550.  
  551. 15. Reiser M., The Oberon System, User Guide and Programmer's Manual ,
  552. Addison-Wesley, 1991.
  553.  
  554. 16. Eberle H., Development and Analysis of a Workstation Computer ,
  555. Ph.D. Thesis, ETH Zürich, 1987.
  556.  
  557. 17. Templ J., SPARC Oberon - User's Guide and Implementation , Report
  558. 133, Departement Informatik, ETH Zürich, 1990.
  559.  
  560. 18. Franz M., The Implementation of MacOberon , Report 141, Departement
  561. Informatik, ETH Zürich, 1990.
  562.  
  563. 19. Pfister C., Heeb B., Templ J., Oberon Technical Notes , Report 156,
  564. Departement Informatik, ETH Zürich, 1991.
  565.  
  566. 20. Crelier R., OP2: A Portable Oberon Compiler , Report 125,
  567. Departement Informatik, ETH Zürich, 1990.
  568.  
  569.